using System;
using System.Collections.Generic;
using System.Linq;

namespace TicTacToe.Core
{
    public class TicTacToeGame
    {
        public event EventHandler<TurnEventArgs> OnTurn;
        public event EventHandler<TurnEventArgs> OnUndoTurn;
        public event EventHandler<GameOverEventArgs> OnGameOver;

        private readonly List<Turn> _gameTurns;
        private readonly int[][][] _tm;

        private static int MaxTurnCount
        {
            get
            {
                return 64;
            }
        }

        public Player PlayerOne { get; private set; }
        public Player PlayerTwo { get; private set; }

        /// <summary>
        /// True if the game is over
        /// </summary>
        public bool IsOver { get; private set; }

        /// <summary>
        /// Get the player who must make the next turn
        /// </summary>
        public Player NextPlayer { get; private set; }

        /// <summary>
        /// The count of turns already made
        /// </summary>
        public int CountTurns
        {
            get
            {
                return _gameTurns.Count;
            }
        }

        public IEnumerable<Turn> Turns
        {
            get { return _gameTurns.ToArray(); }
        }

        #region Methods

        public bool IsValidTurn(Turn turn)
        {
            return IsValidTurn(turn.X1, turn.X2, turn.X3);
        }

        public bool IsValidTurn(int x1, int x2, int x3)
        {
            var inBounds = new[] { x1, x2, x3 }.All(n => n < 4);
            var freeField = !_gameTurns.Select(n => new { n.X1, n.X2, n.X3 })
                            .Any(n => x1 == n.X1 && x2 == n.X2 && x3 == n.X3);

            return inBounds && freeField;
        }

        public void UndoTurn()
        {
            if (CountTurns == 0)
                return;

            if (IsOver)
                throw new InvalidOperationException("Cannot undo turns after game is over.");

            var turn = _gameTurns[_gameTurns.Count - 1];

            _gameTurns.Remove(turn);
            NextPlayer = NextPlayer == PlayerOne ? PlayerTwo : PlayerOne;

            if (OnUndoTurn != null) OnUndoTurn(this, new TurnEventArgs(turn));
        }

        private void TriggerGameOver(int winningPlayerNumber, params int[] coords)
        {
            IsOver = true;

            var winningPlayer = (winningPlayerNumber == 1) ? PlayerOne : PlayerTwo;
            var losingPlayer = (winningPlayerNumber == 2) ? PlayerOne : PlayerTwo;
            var args = new GameOverEventArgs(winningPlayer, losingPlayer, coords);

            if (OnGameOver != null)
                OnGameOver(this, args);
        }

        private void UpdateGameState()
        {
            if (CountTurns < 7)
                return;

            for (int i = 0; i < 4; ++i)
            {
                for (var j = 0; j < 4; j++)
                {
                    // horizontal 1
                    var a = _tm[i][j][0];
                    if (a != 0 && a == _tm[i][j][1] && a == _tm[i][j][2] && a == _tm[i][j][3])
                    { TriggerGameOver(a, i, j, 0, i, j, 1, i, j, 2, i, j, 3); }
                    // horizontal 2
                    var b = _tm[i][0][j];
                    if (b != 0 && b == _tm[i][1][j] && b == _tm[i][2][j] && b == _tm[i][3][j])
                    { TriggerGameOver(b, i, 0, j, i, 1, j, i, 2, j, i, 3, j); }

                    // vertical
                    var c = _tm[0][i][j];
                    if (c != 0 && c == _tm[1][i][j] && c == _tm[2][i][j] && c == _tm[3][i][j])
                    { TriggerGameOver(c, 0, i, j, 1, i, j, 2, i, j, 3, i, j); }
                }

                // diagonal 1
                var d1 = _tm[0][0][i];
                if (d1 != 0 && d1 == _tm[1][1][i] && d1 == _tm[2][2][i] && d1 == _tm[3][3][i])
                { TriggerGameOver(d1, 0, 0, i, 1, 1, i, 2, 2, i, 3, 3, i); }

                d1 = _tm[3][0][i];
                if (d1 != 0 && d1 == _tm[2][1][i] && d1 == _tm[1][2][i] && d1 == _tm[0][3][i])
                { TriggerGameOver(d1, 0, 0, i, 1, 1, i, 2, 2, i, 0, 3, i); }

                //diagonal 3
                var d3 = _tm[i][0][0];
                if (d3 != 0 && d3 == _tm[i][1][1] && d3 == _tm[i][2][2] && d3 == _tm[i][3][3])
                { TriggerGameOver(d3, i, 0, 0, i, 1, 1, i, 2, 2, i, 3, 3); }

                d3 = _tm[i][3][0];
                if (d3 != 0 && d3 == _tm[i][2][1] && d3 == _tm[i][1][2] && d3 == _tm[i][0][3])
                { TriggerGameOver(d3, i, 2, 0, i, 1, 1, i, 0, 2, i, 0, 3); }

                //diagonal 4
                var d4 = _tm[0][i][0];
                if (d4 != 0 && d4 == _tm[1][i][1] && d4 == _tm[2][i][2] && d4 == _tm[3][i][3])
                { TriggerGameOver(d4, i, 0, 0, i, 1, 1, i, 2, 2, 3, i, 3); }

                d4 = _tm[3][i][0];
                if (d4 != 0 && d4 == _tm[2][i][1] && d4 == _tm[1][i][2] && d4 == _tm[0][i][3])
                { TriggerGameOver(d4, i, 2, 0, i, 1, 1, i, 0, 2, 0, i, 3); }
            }

            // diagonal 2
            var d2 = _tm[0][0][0];
            if (d2 != 0 && d2 == _tm[1][1][1] && d2 == _tm[2][2][2] && d2 == _tm[3][3][3])
                TriggerGameOver(d2, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3);

            d2 = _tm[3][0][0];
            if (d2 != 0 && d2 == _tm[2][1][1] && d2 == _tm[1][2][2] && d2 == _tm[0][3][3])
                TriggerGameOver(d2, 0, 2, 2, 1, 1, 1, 2, 0, 0, 0, 3, 3);

            d2 = _tm[0][3][0];
            if (d2 != 0 && d2 == _tm[1][2][1] && d2 == _tm[2][1][2] && d2 == _tm[3][0][3])
                TriggerGameOver(d2, 0, 2, 0, 1, 1, 1, 2, 0, 2, 3, 0, 3);

            d2 = _tm[3][3][0];
            if (d2 != 0 && d2 == _tm[2][2][1] && d2 == _tm[1][1][2] && d2 == _tm[0][0][3])
                TriggerGameOver(d2, 2, 2, 0, 1, 1, 1, 0, 0, 2, 0, 0, 3);

            if (CountTurns == MaxTurnCount)
            {
                IsOver = true;

                if (OnGameOver != null)
                    OnGameOver(this, new GameOverEventArgs());
            }
        }

        /// <summary>
        /// Make a turn
        /// </summary>
        /// <param name="turn">The turn</param>
        public void MakeTurn(Turn turn)
        {
            if (_gameTurns.Count != 0)
            {
                if (IsOver)
                    throw new InvalidOperationException();

                if (NextPlayer != turn.Player)
                    throw new InvalidOperationException();
            }

            if (!IsValidTurn(turn))
                if (turn.ToArray().Any(n => n < 0 || n > 3))
                    return;
                else
                    throw new InvalidOperationException();

            _gameTurns.Add(turn);
            _tm[turn.X1][turn.X2][turn.X3] = (turn.Player == PlayerOne) ? 1 : 2;
            NextPlayer = turn.Player == PlayerOne ? PlayerTwo : PlayerOne;

            if (OnTurn != null)
                OnTurn(this, new TurnEventArgs(turn));

            UpdateGameState();
        }

        #endregion

        public TicTacToeGame(Player playerOne, Player playerTwo)
        {
            PlayerOne = playerOne;
            PlayerTwo = playerTwo;

            NextPlayer = PlayerOne;

            IsOver = false;

            _gameTurns = new List<Turn>();
            _tm = new int[][][]
            {
                new int[][]
                {
                    new int[4],
                    new int[4],
                    new int[4],
                    new int[4]
                },
                new int[][]
                {
                    new int[4],
                    new int[4],
                    new int[4],
                    new int[4]
                },
                new int[][]
                {
                    new int[4],
                    new int[4],
                    new int[4],
                    new int[4]
                },
                new int[][]
                {
                    new int[4],
                    new int[4],
                    new int[4],
                    new int[4]
                }
            };
        }
    }
}